7. Struktury danych - tablice i obiekty

Wyzwania:

  • zainstalujesz i skonfigurujesz narzędzie sprawdzające poprawność kodu JS,
  • dowiesz się, czym są tablice i obiekty,
  • nauczysz się korzystać z szablonów HTML,
  • dokończysz swoją aplikację bloga.

Wstęp

W poprzednim module pojawiło się sporo nowych informacji. W tym tygodniu będziemy korzystać z nich do dokończenia naszego bloga. Rozszerzymy też nieco naszą znajomość JS – nie będzie aż tylu nowości, a nowa wiedza będzie mocno związana z tym, czego do tej pory się nauczyliśmy.

Zaczniemy jednak od skupienia się przez chwilę na jakości naszego kodu JS.

7.1. Dbamy o jakość kodu

W poprzednich modułach poruszaliśmy już to, jak istotne jest dbanie o jakość naszego kodu. Mówimy tu zarówno o poprawności kodu JS, jak i stosowaniu dobrych praktyk i właściwego formatowania kodu. Dlatego właśnie, zanim przejdziemy do dalszych prac, skonfigurujemy w naszym środowisku projektu "pomocnika". Dzięki niemu będzie nam łatwiej pilnować poprawności kodu JS.

Dlaczego przywiązujemy do tego tak dużą wagę? Czy chodzi tylko o to, żeby podążać za modą? Absolutnie nie! Kiedy rozpoczniesz swoją pierwszą pracę jako Junior Web Developer, najpewniej dołączysz do zespołu pracującego nad jakimś projektem. Są spore szanse, że w każdym projekcie spotkasz się z podobną konfiguracją zasad formatowania kodu.

Jest to bardzo często stosowane rozwiązanie, aby wszyscy developerzy pracujący nad projektem stosowali te same zasady. Inaczej ryzykowalibyśmy bałaganem w kodzie. Co gorsza, jeśli jeden z developerów skonfigurował sobie edytor tak, aby np. poprawiał "błędne" wcięcia (czyli inne, niż ten developer używa), to jego commity będą zawierać modyfikacje sporych części edytowanych plików. Wynika to z faktu, że dla Gita zmiana wcięcia np. z 4 na 2 spacje jest zmianą całej linii kodu. Będzie to prowadzić to konfliktów w repozytorium i długich godzin spędzonych na posprzątaniu tego bałaganu.

Dlatego zależy nam, żeby od początku wyrobił się w Tobie nawyk trzymania się wyznaczonych zasad formatowania kodu. Dzięki temu unikniesz w przyszłości przykrych niespodzianek.

Konfiguracja ESLint

W tej chwili najbardziej nam zależy na jakości kodu JS. Jeżeli nie masz jeszcze zainstalowanego narzędzia ESLint, zrób to teraz wedle poniższej instrukcji.

Opcjonalnie, możesz zainstalować również pozostałe narzędzia, które znajdziesz na pozostałych zakładkach poniżej. Pamiętaj jednak, aby nie tracić w tej chwili zbyt dużo czasu na poprawianie kodu SCSS lub HTML, kosztem dalszej nauki JS-a.

ESLint

To narzędzie służy do sprawdzania poprawności składni oraz formatowania kodu JS. W naszym task runnerze będzie automatycznie uruchamiane przy każdym wykonaniu komend: npm run build, npm run watch, oraz npm run test.

Pamiętaj, aby codziennie wyłączać npm run watch i włączać ponownie następnego dnia. Dzięki temu będziesz regularnie sprawdzać poprawność składni i formatowania.

W konfiguracji zawartej w pliku .eslintrc.json, który za chwilę stworzymy, znajdzie się tylko kilka podstawowych zasad:

  • poprawne formatowanie, zgodne z tym ustawionym w EditorConfig (wcięcia, zakończenia linii),
  • używamy pojedynczych cudzysłowów,
  • stawiamy średniki na końcu każdej linii (poza wyjątkami).
Zmiany w task runnerze

Aby używać ESLinta, potrzebujemy wprowadzić jeszcze parę zmian w naszym task runnerze. Otwórz plik package.json i pod taskiem test:html dodaj następującą linię:

"test:js": "eslint js/",

Upewnij się też, że Twój task test wygląda następująco:

"test": "npm-run-all test:*",

Natomiast task watch powinien teraz wyglądać tak:

"watch": "npm-run-all build:* build-dev -p watch:*",
Instalacja

Zainstaluj pakiet eslint za pomocą komendy:

npm install --save-dev eslint

Tworzenie plików z kropką

Za chwilę będziemy potrzebowali stworzyć plik, którego nazwa ma zaczynać się od kropki. Jak już wcześniej wspominaliśmy, może to być nieco problematyczne – szczególnie dla użytkowników Windowsa.

Przypominamy, że najprostszą metodą będzie skorzystanie z terminala (pod Windowsem – z GitBasha). Komenda touch .somefile stworzy nam plik o nazwie .somefile.

Plik ten może być domyślnie ukryty w Twojej przeglądarce plików – sprawdź jej ustawienia, aby wyświetlić ukryte pliki.

W katalogu swojego projektu stwórz nowy plik .eslintrc.json i wklej poniższą zawartość:

{
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": 2015
    },
    "rules": {
        "indent": [
            "error",
            2
        ],
        "linebreak-style": [
            "off"
        ],
        "quotes": [
            "error",
            "single",
            {"allowTemplateLiterals": true}
        ],
        "semi": [
            "error",
            "always"
        ],
        "no-console": [
            "off"
        ]
    }
}
Uruchomienie ESLinta

Spróbuj uruchomić teraz npm run test:js i zobacz, jakie błędy znalazł ESLint w Twoim kodzie. Postaraj się usunąć wszystkie z nich.

Pamiętaj, że w naszej konfiguracji ESLint będzie sprawdzał wszystkie pliki w katalogu js. Jeśli podłączasz jakiekolwiek biblioteki lub pluginy, umieść je w katalogu vendor.

Task build może wyświetlać błędy!

Jeśli ESLint znajdzie jakiekolwiek problemy w Twoim kodzie, to spowoduje błąd, który zatrzyma uruchomionego taska. ESLint jest uruchamiany przez task test:js, który jest wykonywany przez task test. Z kolei ten task jest wykonywany przez build, więc jeśli wykonasz npm run build, to NPM wyświetli błędy (linie w terminalu zaczynające się od npm ERR!).

Jest to zupełnie normalne zachowanie, ponieważ rolą ESLinta jest uniemożliwić dalsze wykonywanie taska, w ramach którego zostało uruchomione sprawdzenie kodu.

Właśnie dlatego w tasku watch zmieniliśmy build na build:* – gdybyśmy nie wprowadzili tej zmiany, nie dałoby się uruchomić taska watch, dopóki nie zostałyby rozwiązane wszystkie problemy wskazane przez ESLinta.

Dla ambitnych

Jeśli masz ochotę nieco usprawnić sobie pracę, możesz zastosować jedno z dwóch rozwiązań:

Opcja 1. – w swoim edytorze kodu zainstalować plugin do obsługi ESLinta. Dzięki temu bezpośrednio w edytorze będą wyświetlały się błędy znalezione przez ESLinta. Ułatwi Ci to usuwanie błędów na bieżąco, w trakcie pisania kodu. Takie pluginy istnieją dla większości popularnych edytorów, wystarczy że wyszukasz nazwę edytora z dopiskiem "ESLint".

Opcja 2. – a może wolisz widzieć błędy w terminalu, ale chcesz, aby automatycznie wyświetlały się po każdym zapisaniu pliku JS? Możesz to osiągnąć instalując pakiet eslint-watch i używając go z flagami --watch --changed --clear (sprawdź w dokumentacji, co robią te flagi). Pamiętaj jednak, aby nie zastępować taska test:js – zamiast tego stwórz nowego taska watch:eslint.

Oba rozwiązania pozwolą Ci na bieżąco rozwiązywać problemy pojawiające się w Twoim kodzie JS.

EditorConfig

Dzięki plikowi .editorconfig, który za chwilę stworzymy, Twój edytor kodu może automatycznie stosować spójne formatowanie kodu we wszystkich plikach. W tym pliku zapisanych jest kilka zasad, takich jak:

  • stosowanie wcięć (indentacji) w postaci 2 spacji,
  • kodowanie pliku utf-8 i unixowe zakończenia linii,
  • usuwanie spacji na końcu linii,
  • dodawanie pustej linii na końcu pliku.

W najpopularniejszych edytorach kodu, takich jak Visual Studio Code, Sublime Text, PhpStorm/WebStorm czy Notepad++, do korzystania z EditorConfig będzie potrzebne zainstalowanie wtyczki. Linki do instrukcji instalacji wtyczek znajdziesz na stronie EditorConfig.

Jak poznać, że EditorConfig działa? Przejdź na koniec dowolnego pliku w swoim projekcie. Jeśli na końcu pliku są jakieś puste linie, usuń je. Na końcu ostatniej linii pliku dodaj kilka spacji. Następnie zapisz plik – jeśli wszystko działa poprawnie, spacje na końcu linii zostały usunięte, a na końcu pliku została dodana pusta linia.

Dzięki temu rozwiązaniu nie musisz pamiętać o poprawnym ustawieniu swojego edytora.

Instalacja

W katalogu swojego projektu stwórz nowy plik .editorconfig i wklej poniższą zawartość:

root = true

[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
core.autocrlf = true

StyleLint

To bardzo podobne narzędzie do ESLinta – jednak zamiast JS, będzie sprawdzać nasze style, a konkretniej pliki w katalogu sass.

Konfiguracja .stylelintrc.json, którą za chwilę zapiszemy, zawiera m.in. następujące zasady:

  • poprawne formatowanie, zgodne z tym ustawionym w EditorConfig (wcięcia, zakończenia linii),
  • wymagana spacja po dwukropku,
  • wymagana nowa linia po średniku,
  • nie więcej niż dwie puste linie bezpośrednio po sobie,
  • maksymalne zagnieżdżenie 4 poziomów (nie licząc pseudoklas),
  • nie używamy znaku & bez potrzeby,
  • nie definiujemy wielokrotnie tej samej zmiennej.
Zmiany w task runnerze

Aby używać ESLinta, potrzebujemy wprowadzić jeszcze parę zmian w naszym task runnerze. Otwórz plik package.json i pod taskiem test:js (dodanym przy konfiguracji ESLinta) dodaj następującą linię:

"test:scss": "stylelint sass/",
Instalacja

Zainstaluj pakiety stylelint i stylelint-scss za pomocą komendy:

npm install --save-dev stylelint stylelint-scss

W katalogu swojego projektu stwórz nowy plik .stylelintrc.json i wklej poniższą zawartość:

{
  "plugins": [
    "stylelint-scss"
  ],
  "rules": {
    "block-no-empty": true,
    "color-no-invalid-hex": true,
    "comment-empty-line-before": null,
    "declaration-colon-space-after": "always",
    "declaration-block-semicolon-newline-after": "always",
    "declaration-block-trailing-semicolon": "always",
    "indentation": 2,
    "max-empty-lines": 2,
    "max-nesting-depth": [3, {
      "ignore": ["pseudo-classes"]
    }],
    "rule-empty-line-before": [ "always", {
      "except": ["first-nested"],
      "ignore": ["after-comment"]
    } ],
    "unit-whitelist": null,
    "scss/at-extend-no-missing-placeholder": true,
    "scss/selector-no-redundant-nesting-selector": true,
    "scss/no-duplicate-dollar-variables": true
  }
}
Uruchomienie StyleLinta

Uruchom teraz npm run test:scss, aby sprawdzić, czy Twój kod spełnia te zasady. Jeśli pojawią się jakieś błędy, napraw je w swoich plikach.

Podobnie jak przy ESLint, sprawdzane będą tylko pliki w katalogu sass. Jeśli używasz jakichś bibliotek, umieszczaj ich pliki .css w katalogu vendor.


Zapisz commit!

Zanim przejdziemy dalej, zapisz commit. Wszystkie nowo stworzone pliki powinny być umieszczone w repozytorium. Dzięki temu wszyscy uczestnicy projektu (np. Mentor, lub Ty na innym komputerze) będą mieli taką samą konfigurację.

Rozpoczynanie nowego projektu

Od teraz, rozpoczynając nowy projekt, będziesz kopiować do niego już nie tylko plik package.json, ale też plik .eslintrc.json. Jeśli w swoim projekcie użyjesz również opcjonalnych narzędzi, kopiuj również pliki .editorconfig, oraz .stylelintrc.json.

Najlepiej stwórz sobie teraz projekt o nazwie project-template i skopiuj do niego te pliki. Dzięki temu będziesz je mieć zawsze pod ręką. Co więcej, w razie potrzeby dostosowania task runnera do konkretnego projektu, bez obaw zmodyfikujesz te pliki w danym projekcie, nie martwiąc się o to, że te same zmiany skopiujesz do innych projektów.

Teraz kiedy uzbroiliśmy się w narzędzia do pomocy w pisaniu poprawnego kodu, możemy kontynuować pracę nad naszym blogiem.

7.2. Dodajemy tagi do artykułu

W poprzednim module udało nam się stworzyć skrypt, który wyświetla właściwy artykuł po kliknięciu linka w lewej kolumnie. Następnie rozszerzyliśmy skrypt o generowanie listy tych linków.

Kolejnym krokiem jest dodanie tagów do każdego z artykułów. Przejdziemy przez to zadanie wspólnie – za to samodzielnie będziesz wykonywać analogiczne operacje dla autorów postów.

Przypisanie tagów do artykułów

Kiedy zerkniesz na nasz design, przypomnisz sobie, że tagi dla każdego artykułu mają postać listy słów kluczowych, z których każde jest linkiem:

image

Obecnie tagi wpisane są na sztywno w HTML pod każdym postem jako lista nieuporządkowana. Jak się domyślasz, to bardzo nieefektywna praktyka. Chcemy, aby lista była generowania automatycznie przez JavaScript. Zaraz to zrobimy!

Zaczniemy od tego, że do każdego artykułu dodamy jego tagi bezpośrednio w znaczniku <article>. Do zapisania ich użyjemy customowego atrybutu data-tags (customowe atrybuty możemy tworzyć sami w zależności od naszych potrzeb).

Obecnie każdy artykuł rozpoczyna się znacznikiem:

<article class="post active" id="article-1">

Tagi dodamy do niego w ten sposób:

<article class="post active" id="article-1" data-tags="cat cactus scissors">

Jak widzisz, założyliśmy, że pierwszy artykuł mówi o rzeczach, które mogą przebijać balony. ;)

Dodaj teraz tagi do wszystkich artykułów. Pamiętaj, żeby nie były identyczne dla każdego artykułu. Niektóre mogą powtarzać się w większości z nich, inne niech występują rzadziej, a jeszcze inne – tylko raz.

Przygotowanie do pisania skryptu

Teraz naszym zadaniem będzie wyświetlenie tagów na końcu każdego artykułu. Sprawimy, że obecna, wpisana na sztywno struktura:

<div class="post-tags">
  <p><strong>Tags:</strong></p>
  <ul class="list list-horizontal">
    <li><a href="#tag-design">design</a></li>
    <li><a href="#tag-tutorials">tutorials</a></li>
  </ul>
</div>

będzie generowana dynamicznie za pomocą JS.

Szablon skryptu

Naszą funkcję nazwiemy generateTags. Na końcu pliku js/script.js dodaj następujący kod:

function generateTags(){
  /* find all articles */

  /* START LOOP: for every article: */

    /* find tags wrapper */

    /* make html variable with empty string */

    /* get tags from data-tags attribute */

    /* split tags into array */

    /* START LOOP: for each tag */

      /* generate HTML of the link */

      /* add generated code to html variable */

    /* END LOOP: for each tag */

    /* insert HTML of all the links into the tags wrapper */

  /* END LOOP: for every article: */
}

generateTags();

Wewnątrz funkcji generateTags umieściliśmy od razu algorytm skryptu, który mamy napisać.

Przeczytaj algorytm i upewnij się, że dobrze rozumiesz każdy kolejny krok. Zwróć też uwagę na wyrażenia START LOOP i END LOOP, które podpowiadają Ci, gdzie zaczynają się i kończą poszczególne pętle. Wkrótce przekonasz się, jakie to ważne!

Uzupełnienie opcji

Znajdź w swoim kodzie miejsce, w którym zdefiniowaliśmy ustawienia:

const optArticleSelector = '.post',
  optTitleSelector = '.post-title',
  optTitleListSelector = '.titles';

Dodaj kolejną stałą o nazwie optArticleTagsSelector i wartości '.post-tags .list'. Zerknij do kodu HTML: ten selektor wybierze nam listę <ul>, w której będą zawarte tagi poszczególnych artykułów.

Usunięcie przykładowych linków

Nie zapomnij w kodzie HTML usunąć linki znajdujące się na końcu każdego artykułu. Chcemy, aby pozostała pusta lista, czyli:

<div class="post-tags">
  <p><strong>Tags:</strong></p>
  <ul class="list list-horizontal">
  </ul>
</div>

Spróbuj samodzielnie!

Większość operacji w tym skrypcie nie powinna już być dla Ciebie czarną magią. Spróbuj samodzielnie napisać ten skrypt!

Jedyną nową wiedzą, jakiej potrzebujesz, jest rozbicie tekstu zawierającego wiele tagów na kolekcję (a właściwie – tablicę, czyli array). Użyjemy do tego funkcji .split(' '). Użyj console.log, aby zobaczyć, jak ona działa, kiedy zastosujesz ją na tekście zawierającym spacje. Otrzymaną tablicę możesz wykorzystywać tak samo, jak kolekcję elementów znalezionych za pomocą querySelecotrAll, czyli użyć jej w pętli for-of.

Pamiętaj, aby testować swój kod po każdym kroku. Używaj do tego console.log, aby rozumieć, co się zmienia w Twoim kodzie, i czy nowe linie kodu działają zgodnie z założeniem.

Jeżeli któryś krok algorytmu sprawi Ci problem, poniżej zamieszczamy opis wraz z wyjaśnieniami.

Generujemy linki tagów w artykułach

W tym rozdziale krok po kroku wytłumaczymy, w jaki sposób wygenerować linki z tagami w każdym artykule.

Znajdujemy artykuły i ich tagi

1. Zaczynamy od znalezienia wszystkich artykułów. To już dobrze znasz – zajrzyj do funkcji generateTitleLinks w swoim kodzie. Zastosuj taką samą stałą articles.

2. Następnie tworzymy pętlę for-of, w której pojedynczym elementem będzie zmienna article – znów, tak samo jak w generateTitleLinks.

3. Kolejny krok to znalezienie wrappera tagów, w którym mają znaleźć się linki. W bardzo podobny sposób w funkcji generateTitleLinks zapisaliśmy stałą titleList – zmieni się tylko selektor na dodany przed chwilą optArticleTagsSelector oraz miejsce wyszukiwania z document na article. Chcemy bowiem znaleźć wrapper tagów w każdym artykule!

4. Zanim przejdziemy dalej, potrzebujemy jeszcze stworzyć zmienną (nie stałą!) o nazwie html i wartości pustego tekstu, czyli ''. Podobnie jak w funkcji generateTitleLinks, będziemy do niej dodawać kolejne fragmenty kodu HTML.

5. Wreszcie, potrzebujemy odczytać tagi z atrybutu data-tags naszego artykułu. Spójrz, w jaki sposób w funkcji titleClickHandler zapisaliśmy stałą articleSelector. Tu będzie bardzo podobnie – zmieni się tylko obiekt (artykuł zamiast klikniętego elementu) oraz nazwa atrybutu. Zapiszmy zawartość atrybutu do stałej articleTags.

Dzielimy tekst na tablicę wyrazów

Wreszcie coś nowego! ;) Wykonawszy poprzednie kroki, dotarliśmy w naszym kodzie do tego komentarza:

/* split tags into array */

W zmiennej articleTags – jak zapewne już sprawdzasz za pomocą console.log – powinien znajdować się tekst, np. 'cat cactus scissors'. Będziemy chcieli jednak wykonać jakieś operacje dla każdego z nich, więc potrzebujemy je podzielić na osobne "rzeczy".

Do tego posłuży nam funkcja .split(' '), która podzieli ten tekst. Użyje ona podanego argumentu, czyli spacji, jako znaku, który jest granicą pomiędzy poszczególnymi "rzeczami" (w naszym przypadku – między poszczególnymi tagami). Pod powyższym komentarzem dodaj linię kodu:

const articleTagsArray = articleTags.split(' ');

Ale chwila, skoro mamy 3 tagi, to jak mamy zapisać je do jednej stałej? Jak się pewnie domyślasz, z odpowiedzią na to pytanie już się spotkaliśmy. Pamiętasz zapewne funkcję querySelectorAll, która pozwalała nam na znalezienie wielu elementów pasujących do selektora. Użyliśmy jej już nawet w funkcji generateTags – spójrz na stałą articles na początku tej funkcji. Ta stała jest kolekcją wielu elementów, po których przechodzimy (iterujemy) za pomocą pętli.

Tutaj sytuacja będzie bardzo podobna, zmienia się tylko nazwa – zamiast kolekcji elementów, mamy do czynienia z tablicą. Po użyciu console.log na stałej articleTagsArray możesz od razu zorientować się, że mówimy o tablicy – wynik w konsoli będzie zamknięty w nawiasy kwadratowe [ ]. Zapamiętaj to – te nawiasy niedługo znowu się pojawią. ;)

Metafora — tablica

Jak zapewne pamiętasz, porównywaliśmy zmienną do pudełka z etykietą.

Tablicę możemy sobie w takim razie wytłumaczyć, jako bardzo długie pudełko, w którym znajdują się kolejno ułożone mniejsze pudełka.

Możemy łatwo sprawdzić, ile mamy małych pudełek, ale nie mają one etykiet. Jak więc zatem wskazać pudełko, które nas interesuje? Musimy tylko wiedzieć, na którym miejscu w szeregu znajduje się to pudełko. Pozycję danego pudełka (pierwsze, drugie, czwarte...) nazywamy indeksem.

Zamiast wskazywać konkretne pudełko, możemy też wykonać jakieś operacje dla każdego z mniejszych pudełek. Tak właśnie będzie działać pętla!

Teraz możemy zastosować pętlę for-of dokładnie tak samo, jak dla artykułów, czyli:

/*split tags into array */
const articleTagsArray = articleTags.split(' ');

/* for each tag */
for(let tag of articleTagsArray){
  /* generate HTML of the link */

  /* add generated code to HTML variable */
}

Jak widzisz, powyższy fragment jest zdjęciem i nie możesz z niego skopiować kodu JS. Będziemy coraz częściej stosować ten zabieg. Wiemy, że może Ci się to wydawać utrudnianiem Ci nauki, ale zależy nam na tym, aby przyzwyczaić Cię do samodzielnego pisania kodu. W ten sposób dużo więcej zrozumiesz i znacznie szybciej będziesz robić postępy w nauce!

Wewnątrz pętli, zmienna tag będzie treścią pojedynczego tagu. Użyj console.log, aby wyświetlić każdy z tagów osobno. Zauważ, że spacje, które znajdowały się pomiędzy tagami, zostały usunięte. Zajęła się tym funkcja split.

Generowanie linków w artykule

Jak pamiętasz, chcemy osiągnąć taką strukturę:

<div class="post-tags">
  <p><strong>Tags:</strong></p>
  <ul class="list list-horizontal">
    <li><a href="#tag-design">design</a></li>
    <li><a href="#tag-tutorials">tutorials</a></li>
  </ul>
</div>

Dla każdego tagu chcemy wygenerować link, którego atrybut href będzie miał prefix #tag-. Czyli np. dla tagu cat będzie to link:

<li><a href="#tag-cat">cat</a></li>

W bardzo podobny sposób generowaliśmy linki w funkcji generateTitleLinks, więc zapewne sobie poradzisz. Pamiętaj, aby (podobnie jak w tamtej funkcji), dodać wygenerowany kod HTML linka do zmiennej html.

Ostatnim krokiem, który wykonujemy już po zamknięciu pętli iterującej po tagach, będzie dodanie linków do wrappera tagów. Ponownie będziemy wzorować się na funkcji generateTitleLinks.

Dodajemy akcję po kliknięciu w tag

Mamy już nasze linki do tagów – teraz czas na obsłużenie kliknięcia w tag. W tym celu dodaj na końcu pliku następujący szablon:

function tagClickHandler(event){
  /* prevent default action for this event */

  /* make new constant named "clickedElement" and give it the value of "this" */

  /* make a new constant "href" and read the attribute "href" of the clicked element */

  /* make a new constant "tag" and extract tag from the "href" constant */

  /* find all tag links with class active */

  /* START LOOP: for each active tag link */

    /* remove class active */

  /* END LOOP: for each active tag link */

  /* find all tag links with "href" attribute equal to the "href" constant */

  /* START LOOP: for each found tag link */

    /* add class active */

  /* END LOOP: for each found tag link */

  /* execute function "generateTitleLinks" with article selector as argument */
}

function addClickListenersToTags(){
  /* find all links to tags */

  /* START LOOP: for each link */

    /* add tagClickHandler as event listener for that link */

  /* END LOOP: for each link */
}

addClickListenersToTags();

Tym razem nie będziemy już tłumaczyć wszystkich kroków – bez problemu poradzisz sobie z napisaniem tego skryptu, wzorując się na funkcji titleClickHandler i innych fragmentach kodu. Skupimy się za to na tych elementach, które mogą sprawić Ci problemy.

Znajdowanie linków do tagów

Możesz zastanawiać się, w jaki sposób mamy znaleźć wszystkie aktywne linki do tagów, albo wszystkie linki do tego samego tagu. Nie przejmuj się, znasz już sposób na to! Posłużymy się funkcją querySelectorAll!

Jako selektor możemy wykorzystać tzw. selektor atrybutu. Aby znaleźć wszystkie aktywne linki do tagów, użyjemy selektora 'a.active[href^="#tag-"]'. Ta część zamknięta w nawiasach kwadratowych to właśnie selektor atrybutu. W tym wypadku użyliśmy łącznika ^=, który oznacza "atrybut href zaczynający się od "#tag-"". Dzięki temu nie potrzebujemy dodawać klasy do naszych linków!

Podobnie, dla wszystkich linków do tego samego tagu, możemy użyć selektora:

'a[href="' + href + '"]'

Tym razem w środku naszego selektora wstawiliśmy stałą href (pomiędzy znakami +). Dzięki temu znajdziemy wszystkie linki, które mają taki sam atrybut href, jak kliknięty link.

Dlaczego stosujemy takie kombinacje? Czemu nie wykorzystamy tego, że wszystkie linki do tagów są we wrapperze tagów? Wybiegamy tutaj nieco naprzód – chcemy, aby te selektory uchwyciły wszystkie linki do tagów, również te, które w kolejnym submodule wygenerujemy w prawej kolumnie.

Co więcej, chcemy żeby wszystkie linki do tego samego tagu stały się aktywne – nie tylko ten kliknięty.

Wydobycie fragmentu tekstu

Zatrzymaj się, kiedy dotrzesz do tego komentarza:

/* make a new constant "tag" and extract tag from the "href" constant */

Stała href będzie zawierać wartość atrybutu href z linka, który stworzyliśmy wcześniej w funkcji generateTags. Ta wartość to może być np. '#tag-cat'. Stworzyliśmy ten href, łącząc prefiks '#tag-' i zmienną tag. Teraz chcemy wykonać operację odwrotną, czyli na podstawie '#tag-cat' uzyskać samo słowo 'cat'.

Jest wiele sposobów, w jakie możemy osiągnąć ten cel, więc nie zdziw się, jeśli szukając w internecie znajdziesz inne rozwiązania. My posłużymy się funkcją replace, która pozwoli nam zamienić tekst '#tag-' na pusty ciąg znaków. Ta linia kodu będzie wyglądała tak:

const tag = href.replace('#tag-', '');

Jak widzisz, funkcja replace otrzymuje dwa argumenty – szukaną frazę oraz ciąg znaków, którym ma ją zastąpić.

Wywołanie funkcji generateTitleLinks

Na samym końcu funkcji tagClickHandler wywołamy funkcję generateTitleLinks. Tym razem jednak, do jej wywołania dodamy atrybut, który będzie selektorem atrybutów artykułu. Za chwilę przerobimy tę funkcję tak, aby umożliwiała nam znalezienie artykułów tylko z podanym tagiem. Zobaczmy najpierw, jak będzie wyglądać to wywołanie funkcji:

generateTitleLinks('[data-tags~="' + tag + '"]');

Ten selektor – a właściwie fragment selektora – za chwilę będzie użyty w funkcji generateTitleLinks do przefiltrowania artykułów. Użyliśmy w nim łącznika ~=, który możemy odczytać jako "znajdź elementy, które mają atrybut data-tags, który ma w sobie słowo 'tag'".

Ten selektor działa podobnie do znajdowania elementu po klasie – nieważne, czy element ma jedną klasę, czy ma ich wiele, ale jeśli jedną z jego klas jest ta, której szukamy, to ten element pasuje do selektora.

Podobnie w tym przypadku, selektor [data-tags~="cat"] znajdzie zarówno element o atrybucie data-tags="cat", jak i data-tags="cactus cat scissors".

Możesz sobie pomyśleć, że teraz to dopiero nas czeka przeprawa! Do tej pory generowaliśmy listę wszystkich linków, a teraz do tego dochodzi jeszcze jakieś filtrowanie!

Owszem, to może brzmieć jak bardzo wymagające zadanie, ale pozytywnie się zaskoczysz!

Zajrzyj do funkcji generateTitleLinks i znajdź linię, w której definiujemy stałą articles.

const articles = document.querySelectorAll(optArticleSelector);

Do tej pory wyszukiwaliśmy wszystkie artykuły za pomocą selektora optArticleSelector, czyli '.post'. Teraz wystarczy, że nieco zmodyfikujemy selektor, aby znaleźć tylko te artykuły, które będą posiadać poszukiwany tag.

Domyślna wartość argumentu

Zacznijmy jednak od nazwania atrybutu – zmień pierwszą linię definicji funkcji generateTitleLinks na:

function generateTitleLinks(customSelector = ''){

Do tej pory nie spotkaliśmy się znakiem = w deklaracji argumentów funkcji! Wiesz jednak, że ten znak oznacza przypisanie wartości. W przypadku argumentu będzie to przypisanie domyślnej wartości.

Zobacz, jak działa ten przykład – możesz go wkleić bezpośrednio do konsoli w narzędziach developerskich.

function multiplyNumbers(numA, numB = 2){
  return numA * numB;
}

console.log('multiplyNumbers(5, 3) =', multiplyNumbers(5, 3)); // multiplyNumbers(5, 3) = 15
console.log('multiplyNumbers(5) =', multiplyNumbers(5)); // multiplyNumbers(5) = 10

W tym przykładzie argument numB przyjmuje wartość 2, jeśli nie został podany przy wywołaniu funkcji. Podobnie w naszym przypadku, jeśli nie podano argumentu, to customSelector będzie miał wartość '', czyli pustego ciągu znaków.

Wykorzystanie selektora

Funkcja generateTitleLinks ma już argument customSelector, który domyślnie jest pustym ciągiem znaków. Teraz wystarczy zmienić wspomnianą wcześniej linię kodu na:

const articles = document.querySelectorAll(optArticleSelector + customSelector);

W ten sposób uzyskamy np. selektor .post[data-tags~="cat"], jeśli funkcja została wywołana z argumentem '[data-tags~="cat"]'. Jeżeli natomiast nie podano żadnego argumentu, to wyszukamy elementy pasujące do selektora .post, czyli wszystkie artykuły.

Analiza filtrowania

Przestudiuj, co się zmieniło w funkcji generateTitleLinks. Użyj console.log do sprawdzenia, jaką wartość ma atrybut customSelector, i jaką wartość mają złączone teksty optArticleSelector i customSelector. Dzięki temu lepiej zrozumiesz, dlaczego wystarczyła nam zmiana dwóch linii, aby ta funkcja wyświetlała przefiltrowaną listę linków do artykułów.

Pamiętaj, aby jak najczęściej używać console.log podczas pisania JS – zwłaszcza wtedy, gdy uczysz się nowych rzeczy, oraz gdy coś nie działa. Dzięki temu nie musisz zgadywać, co dzieje się w kodzie – odpowiedź masz podaną na tacy!

Zadanie: Dodanie autora

W tym submodule, dla każdego artykułu:

  • dodaliśmy tagi w atrybucie data-tags,
  • wyświetliliśmy te tagi jako linki na końcu artykułu,
  • powiązaliśmy kliknięcie w te linki z wygenerowaniem przefiltrowanej listy artykułów w lewej kolumnie.

Oprócz poprawnego wykonania poleceń z tego submodułu Twoim zadaniem jest wykonanie tego samego dla autorów artykułów, czyli:

  • w każdym artykule dodaj autora w atrybucie data-author (usuń autora z wrappera .post-author),
  • wyświetl autora jako link we wrapperze post-author, pod tytułem artykułu,
  • powiąż kliknięcie w link do autora z wygenerowaniem przefiltrowanej listy artykułów.

Dla uproszczenia niech każdy autor ma tylko imię i nazwisko – bez kropek, myślników czy drugich imion.

Wskazówki

  1. Potrzebujesz napisać funkcję generateAuthors, wzorując się na generateTags,
  2. Funkcja generateAuthors będzie prostsza niż generateTags, ponieważ jest tylko jeden autor artykułu – nie musisz dzielić tego pola funkcją split, ani wykonywać pętli podobnej do tej iterującej po tagach. Dla każdego artykułu będzie tylko jeden link do autora.
  3. Napisz też funkcje addClickListenersToAuthors i authorClickHandler, wzorując się na addClickListenersToTags i tagClickHandler.
  4. Nie musisz w żaden sposób zmieniać funkcji generateTitleLinks – wystarczy, że w funkcji authorClickHandler wywołasz ją z odpowiednim argumentem. Pamiętaj, że w tym wypadku w selektorze atrybutu użyjesz łącznika = zamiast ~=.
  5. Nie zapomnij dodać nowej stałej ustawień – optArticleAuthorSelector.
  6. Usuń przykładową zawartość listy autorów z kodu HTML – nie będzie nam już potrzebna.

7.3. Wyświetlamy chmurę tagów

W tym submodule zajmiemy się wygenerowaniem listy tagów w prawej kolumnie.

image

Każdy tag będzie linkiem, który – podobnie jak w każdym artykule – będzie filtrował listę tytułów w lewej kolumnie. To jeszcze nie będzie chmura tagów – na razie interesuje nas tylko lista unikalnych tagów, w postaci linków. Twój efekt może różnić się od tego na powyższym obrazku, w zależności od tagów dodanych do Twoich artykułów.

Przed chwilą dla każdego z artykułów wygenerowaliśmy linki z tagami tego artykułu. Czy nie moglibyśmy przy okazji takich samych linków dodać do prawej kolumny?

Problem leży w tym, że jeśli dwa artykuły mają ten sam tag, to dodalibyśmy go dwukrotnie. Dlatego musimy zastosować inne podejście.

Parę słów o tablicach

Zanim przejdziemy do rozwiązania tego problemu, musimy nieco lepiej zapoznać się z tablicami. Przypomnimy, że spotkaliśmy się z nimi przy okazji użycia funkcji split. Stąd wiesz już, że tablica jest kolekcją wartości, i możemy po niej iterować za pomocą pętli for-of.

Za chwilę zaczniemy używać tablic w inny sposób niż dotychczas. Przeczytaj w naszym poradniku JS cały rozdział dotyczący tablic.

Jeśli chcesz, możesz stworzyć nowy CodePen i wklejać do niego przykłady z poradnika – to pomoże Ci lepiej zrozumieć tablice, z których za chwilę będziemy korzystać.

Pamiętaj, że informacji z poradnika nie musisz uczyć się na pamięć – możesz mieć go cały czas pod ręką. Oczywiście, poradnik zawiera tylko podstawowe możliwości wykorzystania tablic – ale te informacje w zupełności wystarczą nam do zrealizowania tego etapu projektu.

Wyświetlenie listy tagów

Najpierw dodajmy nową stałą ustawień – optTagsListSelector z wartością .tags.list. Ten selektor pozwoli nam na odnalezienie listy tagów w prawej kolumnie.

Do stworzenia naszej tablicy unikalnych tagów wykorzystamy funkcję generateTags. W poniższym szablonie oznaczyliśmy nowe pozycje algorytmu jako [NEW]. Pozostałe fragmenty funkcji pozostawiamy nieuzupełnione – były elementem poprzedniego submodułu.

function generateTags(){
  /* [NEW] create a new variable allTags with an empty array */
  let allTags = [];

  /* find all articles */

  /* START LOOP: for every article: */

    /* find tags wrapper */

    /* make html variable with empty string */

    /* get tags from data-tags attribute */

    /* split tags into array */

    /* START LOOP: for each tag */

      /* generate HTML of the link */

      /* add generated code to html variable */

      /* [NEW] check if this link is NOT already in allTags */
      if(allTags.indexOf(linkHTML) == -1){
        /* [NEW] add generated code to allTags array */
        allTags.push(linkHTML);
      }

    /* END LOOP: for each tag */

    /* insert HTML of all the links into the tags wrapper */

  /* END LOOP: for every article: */

  /* [NEW] find list of tags in right column */
  const tagList = document.querySelector('.tags');

  /* [NEW] add html from allTags to tagList */
  tagList.innerHTML = allTags.join(' ');
}

Zastanówmy się, co się tutaj dzieje:

1. Dla każdego artykułu znajdujemy jego tagi (kod pod komentarzem /* get tags from data-tags attribute */).

2. Dla każdego z tych tagów jest generowany kod HTML linka (kod pod komentarzem /* generate HTML of the link */).

3. Sprawdzamy, czy dokładnie taki link mamy już w tablicy allTags (kod pod komentarzem /* [NEW] check if this link is NOT already in allTags */).

4. Jeśli go nie mamy, dodajemy go do tej tablicy (kod pod komentarzem /* [NEW] add generated code to allTags array */).

5. Na końcu funkcji znajdujemy listę tagów (kod pod komentarzem /* [NEW] find list of tags in right column */) i dodajemy do niej wszystkie linki znajdujące się w tablicy allTagsLinks, łącząc je ze sobą za pomocą spacji (kod pod komentarzem /* [NEW] add html from allTags to tagList */). Pamiętaj, że te fragmenty muszą znajdować się poza pętlą!

Podsumowując, tablica allTags służy nam tutaj tylko za katalog, który informuje nas, czy dany tag już widzieliśmy, czy jeszcze nie. Jednocześnie jest zbiorem linków (a konkretniej – tekstów, które zawierają kod HTML linków), które później wstawiamy do listy tagów w prawej kolumnie.

Oczywiście, to samo zadanie można zrealizować na mnóstwo innych sposobów, a to jest tylko jeden z nich. Jest to jednak zupełnie dobre rozwiązanie, skoro pozwoliło nam na wygenerowanie listy unikatowych tagów (i linków do nich).

Usunięcie listy tagów z prawej kolumny

Nie zapomnij usunąć z kodu HTML przykładowych linków do tagów. Nie będą nam już potrzebne, ponieważ generujemy tę listę za pomocą funkcji generateTags.

Liczba wystąpień tagu

W celu wyświetlenia chmury tagów będziemy potrzebowali znać liczbę wystąpień danego tagu. Będzie nam też potrzebna największa i najmniejsza liczba wystąpień. Jednak nie wszystko naraz – zaczniemy od wyświetlenia listy wystąpień w każdym z linków w prawej kolumnie. Oznacza to, że będą się one różnić od tagów na końcu artykułu – będziemy musieli osobno je wygenerować.

Zanim je wygenerujemy, musimy jakoś dla każdego tagu zapisać liczbę jego wystąpień. W tym wypadku tablica nie będzie bardzo przydatna, ponieważ dla każdego tagu potrzebujemy przechowywać dwie, powiązane ze sobą informacje – jaki to tag oraz ile razy wystąpił. Do tego celu o wiele bardziej przydadzą nam się obiekty!

Poznajemy obiekty

Obiekty są strukturą danych, podobnie jak tablice (ang. arrays). Mają jednak kilka kluczowych różnic, dzięki którym będą dla nas przydatne w innych sytuacjach.

Metafora — obiekt

Podobnie jak tablicę, obiekt porównamy do pudełka, w którym jest wiele małych pudełek.

W przeciwieństwie do tablicy, w obiekcie te małe pudełka nie są ułożone w jakiejś kolejności – zamiast tego, każde pudełko ma swoją etykietę.

Aby wybrać któreś małe pudełko, zamiast mówić "biorę pierwsze pudełko", powiemy "biorę pudełko z etykietą: zabawki dla kota".

Główną różnicą pomiędzy tablicami (arrays) a obiektami jest sposób przechowywania danych. W tablicy każda wartość miała swój indeks, czyli kolejny numer. W obiekcie każda wartość będzie miała klucz, czyli swoją "nazwę".

Wszystkie niezbędne informacje o obiektach, które będą Ci potrzebne do realizacji tego projektu, znajdziesz w naszym poradniku JS, gdzie umieściliśmy cały rozdział dotyczący tablic.

Jeśli masz ochotę, załóż nowy CodePen (lub wykorzystaj CodePen założony do nauki tablic) i wklej do niego przykłady z poradnika.

Budujemy obiekt liczący tagi

Chcemy zliczać wystąpienia każdego tagu. Do tej pory korzystaliśmy z tablicy allTags, w której wartościami były unikalne linki do tagów.

Ta tablica, po wyświetleniu za pomocą console.log, mogła wyglądać np. tak:

['<li><a href="#tag-code">code</a></li>', '<li><a href="#tag-news">news</a></li>']

Do zliczania wystąpień tagów wykorzystamy obiekt – chcemy, aby wyglądał np. tak:

{
  cat: 3,
  cactus: 1,
  scissors: 2
}

Użyliśmy tutaj wieloliniowego formatu zapisu obiektu (czyli z Enterem pomiędzy każdą parą klucz-wartość). Dzięki temu łatwiej będzie nam zrozumieć, co znajduje się w obiekcie. W kodzie JS również możemy używać tego zapisu przy tworzeniu obiektów.

Możemy zaczynać wprowadzanie zmian. Pamiętaj, że dopóki nie skończymy następnych podrozdziałów, nasz kod nie będzie działał poprawnie.

Zmieniamy tablicę na obiekt

Zacznij od znalezienia tego fragmentu kodu:

/* [NEW] create a new variable allTags with an empty array */
let allTags = [];

Zamiast tablicy chcemy wykorzystać obiekt, więc musimy zmienić ten fragment na:

/* [NEW] create a new variable allTags with an empty object */
let allTags = {};

Zauważ, że zmieniamy też komentarz, aby odpowiadał temu, co mamy w kodzie.

Dodawanie nowych tagów do obiektu

Kolejny fragment, który musimy znaleźć, to:

/* [NEW] check if this link is NOT already in allTags */
if(allTags.indexOf(linkHTML) == -1){
  /* [NEW] add generated code to array allTags */
  allTags.push(linkHTML);
}

Musimy go zmienić, ponieważ teraz korzystamy z obiektu, w którym chcemy zliczać wystąpienia tagów. Zacznijmy od obsłużenia sytuacji, w której w obiekcie allTags nie mamy jeszcze danego tagu. Wtedy licznik wystąpień tego tagu ustawiamy na 1.

/* [NEW] check if this link is NOT already in allTags */
if(!allTags[tag]) {
/* [NEW] add tag to allTags object */
  allTags[tag] = 1;
}

Zwróć uwagę, że w warunku użyliśmy wykrzyknika (!), czyli zastosowaliśmy negację. Dlatego warunek czytamy jako "jeśli allTags NIE MA klucza tag". Więcej o negacjach warunków dowiesz się z naszego poradnika JS.

Zliczanie wystąpień tagu

Jeśli natomiast ten tag już znajduje się w allTags, zwiększymy licznik wystąpień o 1. Dopiszemy więc blok else:

/* [NEW] check if this link is NOT already in allTags */
if(!allTags[tag]) {
  /* [NEW] add tag to allTags object */
  allTags[tag] = 1;
} else {
  allTags[tag]++;
}

W tym bloku użyliśmy operatora inkrementacji – znaki ++ zwiększają liczbę o 1. Więcej informacji znajdziesz w naszym poradniku JS.

Teraz zamknij ostatnią linię kodu w komentarz, aby przestała działać, i dodaj console.log, aby sprawdzić, co znajduje się w obiekcie allTags:

/* [NEW] add HTML from allTags to tagList */
// tagList.innerHTML = allTags.join(' ');
console.log(allTags);

Taki sposób na chwilowe wyłączenie – czy zakomentowanie – fragmentu kodu, jest bardzo często używane przez programistów. Dzięki temu nie musisz kasować linii kodu, która aktualnie nie działa, ale za chwilę będzie Ci potrzebna.

Sprawdź w konsoli, jak wygląda zawartość obiektu allTags. Powinna przypominać strukturę, którą przed chwilą pokazaliśmy, czyli:

{
  cat: 3,
  cactus: 1,
  scissors: 2
}

Generowanie linków do listy tagów

Pozostaje nam wygenerować kod HTML linków do umieszczenia na liście tagów w prawej kolumnie. Przy każdym tagu chcemy wyświetlić w nawiasach liczbę jego wystąpień. W tym celu zamień ten fragment kodu:

/* [NEW] add html from allTags to tagList */
// tagList.innerHTML = allTags.join(' ');
console.log(allTags);

na następujący:

/* [NEW] create variable for all links HTML code */
let allTagsHTML = '';

/* [NEW] START LOOP: for each tag in allTags: */
for(let tag in allTags){
  /* [NEW] generate code of a link and add it to allTagsHTML */
  allTagsHTML += tag + ' (' + allTags[tag] + ') ';
}
/* [NEW] END LOOP: for each tag in allTags: */

/*[NEW] add HTML from allTagsHTML to tagList */
tagList.innerHTML = allTagsHTML;

Zwróć uwagę, że używamy tutaj operatora += do doklejania kolejnego linka do zmiennej allTagsHTML. Możesz poczytać o nim więcej w naszym poradniku JS.

Jak widzisz, zamiast linka generujemy tylko nazwę tagu oraz liczbę jego wystąpień. Popraw ten fragment kodu tak, aby był generowany poprawny kod HTML linka.

Podsumowanie dotychczasowych zmian

Po tej zmianie wszystko powinno działać poprawnie. Lista linków do tagów w prawej kolumnie powinna się znów generować, tym razem jednak z informacją o liczbie wystąpień.

Zostało nam już niewiele pracy – musimy już tylko zróżnicować rozmiary tagów. Na końcu nieco zmienimy style, aby linki do tagów nie wyświetlały się jeden pod drugim, tylko inline'owo.

Zamiana listy tagów w chmurę

Cały czas trzymamy się jednej z dobrych praktyk, która mówi, aby nie zmieniać wyglądu strony bezpośrednio za pomocą kodu JS. Podobnie i tym razem, nie będziemy zmieniać rozmiarów czcionek, tylko nadawać klasy – załóżmy, że będzie ich 5, i będą wpływać na rozmiar czcionki. Pierwszą nazwiemy je tag-size-1, a ostatnią – tag-size-5.

Znalezienie skrajnych liczb wystąpień

Załóżmy, że w momencie generowania linka do tagu cactus wiemy, że występuje on w 7 artykułach. Czy to dużo, czy mało? Aby to ustalić, musimy wiedzieć w ilu artykułach pojawia się najrzadszy z tagów, a w ilu – najbardziej popularny.

W tym celu znajdź ten fragment kodu:

/* [NEW] create variable for all links HTML code */
let allTagsHTML = '';

przed tym fragmentem dodamy następujące linie kodu:

const tagsParams = calculateTagsParams(allTags);
console.log('tagsParams:', tagsParams)

Jeszcze nie mamy takiej funkcji, więc przed funkcją generateTags dodaj jej deklarację. Funkcja ta będzie przyjmować jeden argument, który nazwiemy tags. Jej zadaniem będzie znalezienie najmniejszej i największej liczby wystąpień. Te dwie liczby mają zostać zwrócone w postaci obiektu, który będzie zawierał dwa klucze: max i min.

Zanim przejdziemy dalej, krótkie wyjaśnienie – określenie "params" jest skrótem od parameters, czyli parametrów. To jedno z dość często używanych słów w JS, podobnie jak "opts" dla options czy "elem" dla elements.

Spróbuj samodzielnie

Najpierw spróbuj przemyśleć algorytm działania tej funkcji. Wiesz, że argumentem będzie allTags, którego kluczami są tagi, a wartościami – liczby wystąpienia tagu.

Rezultatem zwracanym przez funkcję ma być obiekt, który może wyglądać np. tak:

{
  min: 2,
  max: 7
}

Podpowiemy, że będzie potrzebna pętla iterująca przez wszystkie tagi w obiekcie podanym jako argument funkcji. Czy domyślasz się, jakie operacje będziemy wykonywać dla każdego tagu?

Aby nie psuć Ci okazji do samodzielnej pracy – która jest najlepszą formą nauki – ukryliśmy rozwiązanie tego problemu pod poniższym guzikiem. Kliknij go, kiedy zechcesz poznać rozwiązanie.

Wcześniej jednak podpowiemy, że do rozwiązania tego problemu przydadzą Ci się dwie funkcje z biblioteki Math – przykłady znajdziesz w naszym poradniku JS. ;)

Dokończenie funkcji calculateTagsParams

Funkcję calculateTagsParams zaczniemy od zdefiniowania stałej params, której wartością będzie obiekt zawierający dwa klucze – max z wartością 0 i min z wartością 999999. Założymy, że nasz skrypt obsłuży maksymalnie milion artykułów z takim samym tagiem – to raczej bezpieczne założenie. ;)

Wyjaśnienia dla zainteresowanych

Ustawiamy takie początkowe wartości max i min, aby wykluczyć szansę na to, że wpłyną one na finalne wartości tych parametrów.

Łatwiej jest zrozumieć max niż min – ustawiamy max na 0, żeby potem podnosić tę wartość od największej liczby wystąpień tagu.

A co z min? Załóżmy przez chwilę, że ustawiliśmy min początkową wartość 1, ale najmniej popularny tag występuje w pięciu artykułach. Na końcu działania skryptu min dalej miałoby wartość 1, a nie 5. Wynika to z faktu, że zmieniamy wartość min tylko i wyłącznie, kiedy liczba wystąpień danego tagu jest mniejsza od min. Dlatego chcemy, aby początkowa wartość min była bardzo duża – z założenia większa niż docelowa wartość.

To właśnie ten obiekt – params – funkcja będzie zwracać na końcu. Możesz od razu dodać return params; na końcu funkcji.

Pomiędzy tymi liniami wykorzystamy pętlę for-in, która będzie iterować przez cały obiekt, przekazany do funkcji jako argument. Jeśli argument funkcji calculateTagsParams nazwaliśmy tags, to pętla będzie wyglądać następująco:

for(let tag in tags){
  console.log(tag + ' is used ' + tags[tag] + ' times');
}

Wewnątrz tej pętli musimy ustawić wartości dla params.max – będzie to tags[tag], ale tylko jeśli ta liczba jest większa niż dotychczasowa wartość params.max.

Jest kilka sposobów, aby to osiągnąć. Przedstawiamy je na poniższych zakładkach. Wybór rozwiązania nie jest oczywisty i zależy w dużej mierze od przyjętej konwencji. Różnica w wydajności pomiędzy tymi metodami jest mała, ale short if może działać o 5-10% wolniej. Przy jednorazowym jego zastosowaniu nie stanowi to jednak istotnej różnicy.

Standardowy if

Najprostszym sposobem będzie standardowy blok if:

if(tags[tag] > params.max){
  params.max = tags[tag];
}
Short if

Krótszą formą może być wykorzystanie short ifa, który opisaliśmy w naszym poradniku JS. Jeśli warunek będzie fałszywy, po prostu przypisze do params.max dotychczasową wartość params.max, czyli nic się nie zmieni.

params.max = tags[tag] > params.max ? tags[tag] : params.max;
Math.max

Kolejnym rozwiązaniem może być wykorzystanie funkcji Math.max, która zwraca największą z podanych liczb.

params.max = Math.max(tags[tag], params.max);

W analogiczny sposób znajdziesz najmniejszą liczbę wystąpień – wystarczy, że zamiast param.max użyjesz param.min. W zależności od wybranego rozwiązania, zmień też > na < lub Math.max na Math.min.

Jeśli wszystko poszło dobrze, funkcja calculateTagsParams jest gotowa i powinna zwracać największą i najmniejszą liczbę wystąpień tego samego tagu.

Oczekiwany efekt

Po dokończeniu funkcji calculateTagsParams w konsoli powinniśmy zobaczyć taką linię:

tagsParams: {max: 10, min: 2}

U Ciebie te liczby będą się zapewne różnić od tego przykładu, ale dzięki temu widzisz, że nasza funkcja z powodzeniem znalazła największą i najmniejszą liczbę wystąpień tagów. Za chwilę użyjemy tych liczb, aby dla każdego tagu wybrać jedną z klas decydujących o rozmiarze jego czcionki.


Wybranie klasy dla tagu

Ostatnim krokiem będzie wybranie jednej z 5 klas. Chcielibyśmy jednak, żeby nasz skrypt był w miarę elastyczny i pozwalał na łatwą zmianę liczby klas wpływających na rozmiar tagu w chmurze, jak również na nazwy tych klas.

Znajdź miejsce w kodzie, w którym zapisujesz ustawienia, czyli stałe, których nazwa zaczyna się od opt. Dodaj kolejne stałe:

  • optCloudClassCount o wartości 5,
  • optCloudClassPrefix o wartości tag-size-.

Następnie, przed deklaracją funkcji generateTags dodaj deklarację nowej funkcji – calculateTagClass, która będzie przyjmować dwa argumenty. Nazwijmy je count i params. Za chwilę zajmiemy się jej napisaniem, ale najpierw zastosujmy ją przy generowaniu linków.

Znajdź teraz fragment kodu odpowiedzialny za generowanie kodu HTML linka dodawanego do chmury tagów. Ta linia kodu powinna zaczynać się od allTagsHTML +=.

W generowanym kodzie HTML linka dodaj atrybut class="". Jako wartość tego atrybutu wstaw wartość zwracaną przez funkcję calculateTagClass, której przekażemy dwa argumenty – liczbę wyświetleń tagu oraz stałą tagsParams.

Jeśli nie masz pewności, jak to zrobić, zacznij od wpisania w nowej linii:

const tagLinkHTML = calculateTagClass(allTags[tag], tagsParams);
console.log('tagLinkHTML:', tagLinkHTML);

Następnie, do wartości tagLinkHTML "doklejaj" kolejne fragmenty tekstu, będącego kodem HTML, np:

const tagLinkHTML = '<li>' + calculateTagClass(allTags[tag], tagsParam) + '</li>';
console.log('tagLinkHTML:', tagLinkHTML);

W ten sposób możesz, krok po kroku, zbudować cały kod HTML linka, który należy wstawić zamiast kodu generowanego do tej pory. Innymi słowy, następnie znajdziesz linię zaczynająca się od allTagsHTML += i zmienisz ją na:

allTagsHTML += tagLinkHTML;

Na razie możesz nadal przy każdym linku wyświetlać liczbę wyświetleń – przyda nam się przy sprawdzaniu, czy rzeczywiście najczęściej występujące tagi mają największy rozmiar czcionki.

Spróbuj samodzielnie

Działanie tej funkcji nie będzie wymagało żadnej nowej wiedzy, więc spróbuj samodzielnie napisać algorytm działania tej funkcji. Poświęć na to nie więcej niż 10-15 minut.

Jeśli uda Ci się napisać algorytm, poświęć kolejne 30 minut na próbę samodzielnego stworzenia kodu tej funkcji.

Czasy, które tutaj podaliśmy, są oczywiście tylko wskazówką. To rozwiązanie wymaga trochę matematycznego myślenia, więc nie chcemy, żeby zbyt dużo czasu zeszło Ci na przypominanie sobie lekcji matmy ze szkoły. ;)

Dokończenie funkcji calculateTagClass

Algorytm działania tej funkcji będzie wymagał odrobiny matematyki. Chcemy dowiedzieć się, która z 5 klas będzie odpowiednia dla danego tagu. Będzie to pierwsza z klas, jeśli liczba wystąpień danego tagu będzie w dolnych 20% zakresu pomiędzy najmniejszą, a największą liczbą wystąpień.

Tworzenie algorytmu

Weźmy na przykład tag, który ma 6 wystąpień. Załóżmy, że params wygląda tak:

{
  min: 2,
  max: 10
}

Skoro najmniejsza liczba wystąpień to 2, to interesuje nas tylko to, o ile więcej wyświetleń ma dany tag. Wynika to z faktu, że interesuje nas tylko, gdzie nasza liczba wystąpień leży pomiędzy minimum a maksimum. Dlatego "znormalizujemy" liczby, czyli sprawimy by odpowiadały na pytanie "jak daleko mam do najmniejszej liczby?".

W naszym przykładzie tag ma o 4 wystąpienia więcej od najmniejszej liczby, a maksimum – 8.

Kiedy podzielimy 4 przez 8 uzyskamy wynik 0.5 czyli 50%. To oznacza, że nasza liczba wystąpień jest dokładnie pośrodku pomiędzy najmniejszą a największą. Z tego wynika, że ten tag będzie miał średni rozmiar – spośród pięciu możliwych klas wybierzemy tag-size-3.

Jak z 0.5 uzyskaliśmy 3 w nazwie klasy? Wystarczy, że skorzystamy z tego samego algorytmu, jaki wykorzystaliśmy przy losowaniu liczby całkowitej. To było dwa moduły temu, więc przypomnimy:

  • mnożymy nasz ułamek przez szerokość zakresu liczb, które chcemy otrzymać,
  • dodajemy najmniejszą liczbę z zakresu.

W naszym przypadku zakresem są liczby od 1 do 5 – a konkretniej do liczby zapisanej w optCloudClassCount. Najmniejszą liczbą jest 1, a szerokość w tym przypadku jest równa największej liczbie – 5. Wynik zaokrąglamy w dół. Czyli 0.5 * 5 + 1 daje wynik 3.5, który po zaokrągleniu w dół da 3.

Sprawdzenie i poprawa algorytmu

Zanim przejdziemy do zapisania kodu JS, warto sprawdzić, czy znaleźliśmy poprawny algorytm. Sprawdza się on dla liczby wystąpień równej 6, ale sprawdźmy nasze rozumowanie dla skrajnych liczb. Wiemy, że minimalna liczba wystąpień to 2, czyli o 0 więcej od minimum. Wtedy podzielimy 0 przez 8 i uzyskamy 0. Podstawiając do powyższego wzoru, 0 * 5 + 1 daje wynik 1. Wszystko się zgadza!

A jak zachowa się nasz algorytm dla maksymalnej liczby wystąpień? Znormalizowana liczba będzie – podobnie jak maksimum – o 8 większa od minimum. Czyli podzielimy 8 przez 8 i uzyskamy 1. Podstawiając do wzoru, 1 * 5 + 1 da wynik 6. To niedobrze – największą z liczb miało być 5!

Błąd wynika z tego, że użyliśmy algorytmu opartego o Math.random, który nigdy nie przyjmie wartości 1 – a w naszym wypadku możemy mieć wartość 1. Dlatego musimy zmniejszyć zakres liczb o 1, czyli zamiast 1 * 5 + 1 będziemy używać 1 * 4 + 1, które daje wynik 5. Po podstawieniu innych wartości zobaczymy, że dla liczby wystąpień równej 2 (minimalna) nadal otrzymamy 1, a dla początkowego przykładu z 6 wynikiem będzie 3.

Uwzględnimy tę zmianę algorytmu, pisząc nasz kod.

Zapisanie algorytmu jako kod JS

Spróbujmy teraz zapisać każde z powyższych obliczeń za pomocą kodu JS. Zaczęliśmy od odjęcia 2 od 6, czyli:

const normalizedCount = count - params.min;

Następnie zmniejszyliśmy 10 – również o 2:

const normalizedMax = params.max - params.min;

W kolejnym kroku podzieliliśmy te dwie liczby – 4 i 8:

const percentage = normalizedCount / normalizedMax;

I wreszcie, zastosowaliśmy algorytm znany z losowania liczby całkowitej:

const classNumber = Math.floor( percentage * (optCloudClassCount - 1) + 1 );

Zauważ, że te każdą z tych stałych omówiliśmy wcześniej. W tym podejściu rozbiliśmy nasz problem na kolejne kroki, i każdy z nich zapisaliśmy jako kod JS.

Inne podejście do zapisania algorytmu w postaci kodu JS

Nie jest to jednak jedyne możliwe podejście – możemy też zacząć od ostatniego obliczenia, które napisaliśmy, wykorzystując przykładowe liczby. Następnie za każdą z nich możemy podstawić to, w jaki sposób ją osiągnęliśmy. W ten sposób, krok po kroku, dojdziemy do finalnej wersji tego równania.

W poniższym przykładzie wielokrotnie wpisujemy tę samą linię kodu. Za każdym razem zmieniamy w niej jedną rzecz, aby zbliżyć się do finalnej wersji tej ewoluującej linii kodu.

classNumber = Math.floor( 0.5 * 5 + 1 );

classNumber = Math.floor( 0.5 * optCloudClassCount + 1 );

classNumber = Math.floor( ( 4 / 8 ) * optCloudClassCount + 1 );

classNumber = Math.floor( ( (6 - 2) / (10 - 2) ) * optCloudClassCount + 1 );

classNumber = Math.floor( ( (count - 2) / (10 - 2) ) * optCloudClassCount + 1 );

classNumber = Math.floor( ( (count - 2) / (params.max - 2) ) * optCloudClassCount + 1 );

classNumber = Math.floor( ( (count - params.min) / (params.max - 2) ) * optCloudClassCount + 1 );

classNumber = Math.floor( ( (count - params.min) / (params.max - params.min) ) * optCloudClassCount + 1 );

Oczywiście, otrzymaliśmy ten sam wynik, co w przypadku poprzedniego sposobu. Jest tylko inaczej zapisany – zamiast rozbicia na poszczególne stałe, zapisaliśmy wszystko w jednej linii.

Możesz pomyśleć, że mogliśmy podejść do tego problemu inaczej – np. podając Ci gotowe rozwiązanie. Mogliśmy też użyć ilustracji czy metafor, aby uprościć Ci zrozumienie problemu. Zależało nam jednak na tym, aby pokazać Ci sposób – a nawet dwa – w jaki developer doszedłby do tego rozwiązania.

Niezależnie od tego, który sposób wybierzesz, ważne aby udało Ci się rozłożyć dany problem na poszczególne kroki, następnie każdy krok zapisać jako kod JS, który w rezultacie wykona algorytm skryptu.

Tak czy inaczej, ten problem już jest za nami. Wystarczy dodać linię, która zwróci z funkcji stałą optCloudClassPrefix i dołączoną do niej stałą classNumber.


Oczekiwany efekt

Jeśli wszystko poszło poprawnie, sprawdzając listę tagów za pomocą inspektora, zobaczysz różne klasy tag-size-X przy różnych tagach w prawej kolumnie:

image

Dopracowanie chmury tagów

Pozostałe kroki wykonaj samodzielnie:

  • zdecyduj, ile chcesz mieć różnych rozmiarów tagów,
  • w SCSS zapisz deklaracje rozmiarów czcionek dla klas tag-size-X (możesz wykorzystać wartości procentowe),
  • jeśli chcesz, klasom tag-size-X możesz też przypisać różne kolory,
  • zmodyfikuj listę tagów tak, aby tagi były wyświetlane inline'owo,
  • usuń liczby wyświetleń z kodu generującego kod HTML linków.

To wszystko! W efekcie na Twojej stronie powinna pojawić się ciekawa chmura tagów, w której najpopularniejsze tagi będą wyświetlone największą czcionką.

Te wszystkie zmiany nie powinny były mieć wpływu na działanie klikania w tag. Sprawdź, czy lista jest nadal tak samo filtrowana.

Zadanie: Lista autorów

W tym submodule, dla każdego artykułu:

  • wygenerowaliśmy listę tagów w prawej kolumnie,
  • policzyliśmy ile razy występuje każdy z tagów,
  • nadaliśmy linkom klasy w zależności od liczby wystąpień danego tagu.

Oprócz poprawnego wykonania poleceń z tego submodułu Twoim zadaniem jest wykonanie analogicznej funkcjonalności dla autorów artykułów, czyli:

  • wygeneruj listę autorów (w postaci linków) w prawej kolumnie,
  • do każdego linka dodaj liczbę artykułów danego autora.

Wskazówki

  1. Zmodyfikuj funkcję generateAuthors, wzorując się zmianach w funkcji generateTags,
  2. Pamiętaj, że jest tylko jeden autor artykułu, więc funkcja generateAuthors ma o jedną pętlę mniej.
  3. Nie zapomnij dodać nowej stałej ustawień – optAuthorsListSelector.

Dla ambitnych

Teraz kiedy znasz już obiekty, możemy Ci powiedzieć, że bardzo często wykorzystuje się je do przechowywania ustawień – czyli tych wartości wykorzystywanych w skrypcie, które możesz chcieć w przyszłości łatwo zmienić.

Dzięki stosowaniu takich ustawień będzie Ci dużo łatwiej dostosować skrypt, jeśli zdecydujesz się na zmianę struktury HTML albo nazewnictwa klas decydujących o rozmiarach tagów w ich chmurze.

Znajdź fragment kodu, w którym deklarujesz stałe o nazwach zaczynających się od opt. Zacznij od przeniesienia ich na samą górę pliku. Dzięki temu łatwo będzie je znaleźć.

Następnie stwórz stałą o nazwie opt i umieść w niej pusty obiekt. Do tego obiektu przenosić poszczególne stałe ustawień. Np. dla takich stałych:

const optArticleSelector = '.post',
  optTitleSelector = '.post-title',
  optTitleListSelector = '.titles';

obiekt ustawień może wyglądać następująco:

const opts = {
  articleSelector: '.post',
  titleSelector: '.post-title',
  titleListSelector: '.titles'
};

Po tej zmianie wystarczy, że w swoim edytorze kodu znajdziesz opcję "znajdź i zamień" i wykorzystasz ją do zmiany wszystkich fraz optArticleSelector na opt.articleSelector. Podobną operację możesz wykonać dla pozostałych ustawień.

Możesz powiedzieć, że ta zmiana jest kosmetyczna – i faktycznie tak jest. Pomoże nam to jednak usystematyzować nazewnictwo naszych ustawień. Jednak ten krok to dopiero początek.

Nowością, o której wcześniej nie mówiliśmy, jest możliwość umieszczania obiektów w obiektach! To oznacza, że Twoje ustawienia mogą wyglądać np. tak:

const opts = {
  tagSizes: {
    count: 5,
    classPrefix: 'tag-size-',
  },
};

const select = {
  all: {
    articles: '.post',
    linksTo: {
      tags: 'a[href^="#tag-"]',
      authors: 'a[href^="#author-"]',
    },
  },
  article: {
    tags: '.post-tags .list',
    author: '.post-author',
  },
  listOf: {
    titles: '.titles',
    tags: '.tags.list',
    authors: '.authors.list',
  },
};

Zwróć uwagę, że w powyższym przykładzie użyliśmy "za dużo" przecinków. W kodzie JS nie jest to błąd, a coraz częściej spotykana praktyka. Wynika to z faktu, że stawiając przecinek po każdym elemencie, możesz dowolnie zmieniać ich kolejność i zawsze dodać kolejny element. W efekcie nie musisz pilnować czy na pewno przecinki są po każdym elemencie, poza ostatnim.

Dzięki takiemu "drzewu" ustawień łatwiej będzie Ci zapamiętać, że:

  • selektor do wszystkich linków do tagów znajdziesz w select.all.linksTo.tags,
  • selektor listy tagów – w select.listOf.tags,
  • liczbę klas regulujących rozmiary czcionek w chmurze tagów – w opts.tagSizes.count.

Ta zmiana pozwoli Ci lepiej rozumieć swój kod JS, kiedy będziesz czytać go za godzinę, dzień czy miesiąc. A jeśli przyjdzie potrzeba zmiany tych ustawień, również łatwiej będzie Ci się w nich odnaleźć.

Jeśli chcesz, możesz dostosować ustawienia swojego skryptu wedle tych wskazówek. Możesz również stworzyć nowe ustawienia, np. na klasę nadawaną aktywnym artykułom czy linkom.

7.4. Szablony Handlebars

Do tej pory, kiedy potrzebowaliśmy za pomocą JS-a dodać nowy element lub tekst na stronie, wpisywaliśmy go w kodzie JS. Nie jest to jednak dobra praktyka. Dlatego nauczymy się wykorzystywać szablony HTML.

Po co nam szablony HTML?

Najłatwiej będzie odpowiedzieć na pytanie, zaczynając od tematu stylów strony. Wspominaliśmy już o tym, że nie chcemy zmieniać wyglądu strony bezpośrednio w kodzie JS. Dlatego nasze skrypty zawsze posługują się klasami, zamiast np. nadawać styl display czy font-size.

Wynika to z prostej zasady – do kontrolowania wyglądu strony służy kod CSS, który generujemy za pomocą SCSS-a. Jeśli w przyszłości ktoś (może nawet my sami) będzie chciał zmienić wygląd np. aktywnego linka, to będzie szukał tego koloru w kodzie SCSS lub ew. CSS, a nie w kodzie JS.

Co więcej, dzięki takiemu podziałowi możemy wykorzystywać nasze skrypty również na innych stronach. Z tego samego względu zapisujemy też ustawienia na początku pliku JS.

Ta niezależność kodu JS i możliwość ponownego wykorzystania go jest – jak do tej pory – niweczona przez fragmenty kodu HTML, znajdujące się w naszym kodzie JS. Całe szczęście, nie mieliśmy potrzeby w JS-ie wpisywać żadnych słów wyświetlanych na stronie, ale prędzej czy później spotkalibyśmy się i z tym przypadkiem. Wtedy nawet inna wersja językowa strony wymagałaby nowej wersji kodu JS.

Rozwiązaniem tych problemów jest właśnie wykorzystanie szablonów HTML.

Implementacja Handlebars

Jest wiele pluginów umożliwiających wykorzystanie szablonów kodu HTML. Jednym z prostszych i bardziej niezawodnych jest Handlebars, z którego za chwilę skorzystamy. Jest on oparty o bardzo popularny Mustache.js, ale oferuje od niego nieco więcej funkcjonalności.

Jak możesz zobaczyć na stronie tego pluginu, najprostszym przykładem szablonu jest:

<script id="entry-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>{{title}}</h1>
    <div class="body">
      {{body}}
    </div>
  </div>
</script>

W pierwszej chwili może Cię zdziwić zastosowanie tagu <script>, co sugerowałoby, że znajduje się w nim kod JS. Musisz jednak wiedzieć, że przeglądarka interpretuje zawartość tagu <script> jako kod JS, tylko jeśli nie podano atrybutu type, lub jego wartość to text/javascript. Właśnie dlatego w tym przypadku podajemy atrybut type="text/x-handlebars-template", aby przeglądarka nie interpretowała zawartości tego tagu.

Dzięki takiemu podejściu możemy w kodzie HTML umieścić ten szablon i nie zostanie on wyświetlony na stronie, ale będziemy mieli do niego dostęp w naszym skrypcie. Wykorzystamy do tego id tagu <script>.

W zawartości tego tagu została wpisana nazwa zmiennej title, zamknięta w podwójnym nawiasie klamrowym {{ }} (po polsku takie nawiasy czasem zwane są "wąsami"). Jak za chwilę zobaczysz, cały fragment {{ title }} zostanie podmieniony na wartość, którą podamy w momencie parsowania szablonu, czyli podstawiania do niego wartości.

Przykłady zawarte w dokumentacji pluginu często korzystają z pluginu jQuery, dlatego sami omówimy sobie implementację szablonów Handlebars.

Podłączenie biblioteki

Stwórz nowy CodePen, który wykorzystamy do nauki używania szablonów Handlebars.

Najpierw musimy podłączyć plik JS z biblioteką Handlebars. W tym celu do kodu HTML w CodePenie wstaw:

<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.0/handlebars.min.js"></script>

Dodanie szablonu

Następnie kodu HTML dodaj następujący szablon:

<script id="template-hello" type="text/x-handlebars-template">
  <p>Hello {{ firstName }} {{ lastName }}!</p>
</script>

Wykorzystanie szablonu

Teraz czas na kod JS – użyj tego kodu, który za chwilę wytłumaczymy. Spróbuj przeczytać ten kod i zrozumieć (a częściowo domyślić się) co robi każda linia kodu.

const tplHelloSource = document.querySelector('#template-hello').innerHTML;
const tplHello = Handlebars.compile(tplHelloSource)
const dataHello = {firstName: 'John', lastName: 'Smith'};
let generatedHTML = tplHello(dataHello);

const targetElement = document.body;
targetElement.insertAdjacentHTML('beforeend', generatedHTML);

console.log('tplHello:');
console.log(tplHelloSource);
console.log('=========');

console.log('dataHello:');
console.log(dataHello);
console.log('=========');

console.log('generatedHTML:');
console.log(generatedHTML);
console.log('=========');

Omówmy kolejne kroki zastosowania naszego pierwszego szablonu:

  • wartością tplHelloSource jest zawartość elementu o id template-hello, czyli naszego szablonu,
  • w następnej linii za pomocą Handlebars.compile tworzymy funkcję tplHello, która posłuży nam do wykorzystywania tego szablonu,
  • zmienna dataHello to obiekt, który zawiera teksty do wyświetlenia,
  • w zmiennej generatedHTML znajdzie się szablon, w którym fragmenty w podwójnych nawiasach klamrowych zostały zamienione na wartości z obiektu dataHello,
  • kolejne dwie linie kodu znajdują pierwszy artykuł, i wstawiają na początku artykułu wygenerowany kod HTML.

Jedyna nowość to obiekt Handlebars, który zawiera funkcję (czyli metodę) compile. Wywołujemy ją, podając dwa argumenty – szablon tplHello oraz dane dataHello.

Oczywiście, ten sam szablon możemy wykorzystać wielokrotnie z różnymi zestawami danych, np. dodając:

const dataHello2 = {firstName: 'Maria', lastName: 'Jones'};
generatedHTML = tplHello(dataHello2);
targetElement.insertAdjacentHTML('beforeend', generatedHTML);

Pętle w szablonach Handlebars.js

Znasz już podstawy używania szablonów. Co jednak gdybyśmy chcieli wygenerować całą listę, włącznie z <ul>? Bazując na dotychczasowych informacjach, musielibyśmy stworzyć dwa szablony – jeden dla listy <ul>, oraz drugi dla pojedynczego elementu <li>.

Autorzy tej biblioteki przewidzieli taką sytuację. Dzięki temu możemy zawrzeć pętlę bezpośrednio w szablonie!

Dodaj do kodu HTML taki szablon:

<script id="template-product-list" type="text/x-handlebars-template">
  <h3>{{ title }}</h3>
  <ul>
    {{#each products}}
    <li id="{{ @key }}">Buy {{ name }} for {{ price }}!</li>
    {{/each}}
  </ul>
</script>

W tym szablonie tagi {{#each products}} i {{/each}} wyznaczają początek i koniec pętli, która będzie iterować po tablicy zawartej w kluczu products. Jako id każdego <li> użyliśmy klucza każdego produktu. Lepiej zrozumiesz to, analizując kod JS, który wykorzystuje ten szablon. Możesz dodać go na końcu kodu JS w swoim CodePenie.

const tplProductListSource = document.querySelector('#template-product-list').innerHTML;
const tplProductList = Handlebars.compile(tplProductListSource);

const productListData = {
  title: 'Great offers!',
  products: {
    'product-football': {
      name: 'Football',
      price: '$10'
    },
    'product-volleyball': {
      name: 'Volleyball',
      price: '$8'
    },
    'product-basketball': {
      name: 'Basketball',
      price: '$12'
    }
  }
};

generatedHTML = tplProductList(productListData);
targetElement.insertAdjacentHTML('beforeend', generatedHTML);

Jak widzisz, productListData jest obiektem. Zawiera w sobie klucz products, który zawiera obiekt. Elementami tego obiektu są obiekty. Każdy z nich zawiera klucze name i price, które wykorzystaliśmy w naszym szablonie. Klucze produktów, np. 'product-football', są przez nas używane jako id każdego <li> za pomocą wyrażenia {{ @key }}.

Dodaj ten przykład do swojego CodePena i zmieniaj w nim wartości w obiekcie productListData, aby lepiej zrozumieć, jak te zmiany przekładają się na wygenerowany kod HTML.

Chwila, czy nie mówiliśmy przed chwilą, że nie chcemy mieć w kodzie JS żadnych słów wyświetlanych na stronie? Trafna uwaga! W praktyce są dwa zastosowania, przydatne w zależności od sytuacji:

  1. obiekt productListData mógł być pobrany przez kod JS z serwera – ten przypadek będziemy niedługo przerabiać, ucząc się o AJAX i API,
  2. obiekt productListData może też znajdować się w kodzie HTML.

Dla ćwiczenia, w swoim CodePenie usuń ten obiekt z kodu JS i umieść go w kodzie HTML pomiędzy nowymi tagami <script></script>. Dzięki temu wszystkie teksty znajdą się w kodzie HTML, a nasz skrypt będzie miał do nich dostęp, aby wygenerować odpowiedni fragment kodu do wstawienia na stronie.

Zadanie: Wykorzystanie szablonów w projekcie

Czas użyć szablonów HTML w naszym blogu!

Skrypt, który do tej pory napisaliśmy, generuje fragmenty kodu HTML w pięciu miejscach:

  1. dla linka do artykułu, umieszczanego w lewej kolumnie,
  2. dla linka do tagu, umieszczanego na końcu każdego artykułu,
  3. dla linka do autora, umieszczanego pod tytułem każdego artykułu,
  4. dla linka do tagu, umieszczanego w chmurze tagów w prawej kolumnie,
  5. dla linka do autora, umieszczanego na liście w prawej kolumnie.

Dla pozycji 1-3 zastosuj szablony pojedynczego linka, czyli wedle przykładu wykorzystującego tplHello.

Dla pozycji 4-5 zastosuj szablony zawierające pętlę. Aby je wykorzystać, w swoim kodzie musisz odnaleźć miejsca, w których generujesz kod tych linków. Zamiast generować kod, stwórz obiekt i wypełniaj go danymi w takiej strukturze, jakiej będzie wymagał szablon.

Handlebars a ESLint

ESLint domyślnie zakłada, że elementy, które znajdują się poza danym plikiem, nie mogą być w nim użyte, dopóki ich nie zaimportujemy (np. przy użyciu instrukcji import). Nie bierze jednak pod uwagę, że dodając do pliku HTML kilka plików JS przy użyciu tagu <script>, mogą one korzystać nawzajem ze swojej zawartości i wtedy wcale nie musimy niczego importować. Na przykład, gdy dodamy do index.html informację o a.js i b.js, w tym drugim pliku będziemy mogli korzystać z pierwszego bez żadnych problemów.

Tym samym może dojść do sytuacji, w której Twój kod działa poprawnie, a jednak ESLint wyrzuca błąd mówiący, że Handlebars są niezdefiniowane. Możemy jednak łatwo to naprawić. Wystarczy poinformować ESLint, że jest to coś globalnego i powinien zakładać, że każdy plik JS ma do niego dostęp. Możesz to zrobić, dodając do pliku eslintrc.json regułę:

"globals": {
  "Handlebars": false
}

Jak zastosować Handlebars w projekcie?

Zaczynamy od dodania skryptu Handlebars. W kodzie HTML znajdź tę linię kodu:

<script src="js/script.js"></script>

I nad nią umieść tę linię:

<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.0/handlebars.min.js"></script>

Ten skrypt dodajemy tylko raz, niezależnie od tego ilu szablonów będziemy używać.

Teraz przed tą linią wstaw swój pierwszy szablon:

<script id="template-article-link" type="text/x-handlebars-template">
  <li><a href="#{{ id }}"><span>{{ title }}</span></a></li>
</script>

Następnie na początku swojego pliku JS wstaw ten kod:

const templates = {
  articleLink: Handlebars.compile(document.querySelector('#template-article-link').innerHTML)
}

Dodając później kolejne szablony, będziesz rozbudowywać obiekt templates, aby wszystkie mieć zebrane w jednym miejscu. Wystarczy, że skopiujesz tę linię i zmienisz klucz articleLink i selektor #template-article-link.

Kolejnym krokiem jest wykorzystanie szablonu – zacznij od znalezienia tej linii kodu:

const linkHTML = '<li><a href="#' + articleId + '"><span>' + articleTitle + '</span></a></li>';

Zamień ją na następującą:

const linkHTMLData = {id: articleId, title: articleTitle};
const linkHTML = templates.articleLink(linkHTMLData);

W ten sposób wykonaliśmy pkt. 1 tego zadania. Punkty 2 i 3 wykonaj, powtarzając te same kroki.

Dla punktów 4 i 5 będzie odrobinę trudniej. Omówmy sobie pokrótce wykonanie pkt. 4.

1. Zacznij od dodania pustego szablonu w kodzie HTML oraz w obiekcie templates. Założymy, że ten szablon będzie miał klucz tagCloudLink, czyli będziemy go używać jako templates.tagCloudLink.

2. Najpierw znajdujesz let allTagsHTML = ''; – to zmienna, w której przechowujemy kod HTML wszystkich linków do tagów. Nie będzie już nam potrzebna, a zamiast niej chcemy mieć:

const allTagsData = {tags: []};

3. Następnie znajdujemy linię allTagsHTML += tagLinkHTML;. Ta linia doklejała kod kolejnego linka do allTagsHTML. Zamiast tego, chcemy do tablicy allTagsHTML dodawać kolejny obiekt, np.:

allTagsData.tags.push({
  tag: tag,
  count: allTags[tag],
  className: calculateTagClass(allTags[tag], tagsParams)
});

4. Teraz znajdujemy linię tagList.innerHTML = allTagsHTML; – nie mamy już allTagsHTML, więc zamieniamy tę linię na:

tagList.innerHTML = templates.tagCloudLink(allTagsData);

To jest też dobre miejsce, aby za pomocą console.log sprawdzić, jak wygląda zawartość allTagsData.

5. Wszystko powinno działać, ale do tej pory nasz szablon był pusty. Zacznij od wstawienia w nim jakiegoś tekstu, aby sprawdzić, czy ten tekst wyświetli się tylko raz na stronie. Jeśli tak, wszystko działa poprawnie. Teraz w naszym szablonie potrzebujemy zrobić pętlę. Zacznij od takiego szablonu:

<script id="template-tag-cloud-link" type="text/x-handlebars-template">
  {{#each tags}}
  <li>{{ tag }}</li>
  {{/each}}
</script>

Na stronie, w prawej kolumnie, powinna pojawić się lista tagów. Nie są one jednak jeszcze linkami, i nie mają klas odpowiedzialnych za rozmiar tagu. Aby sobie z tym poradzić, użyj właściwości count i className w swoim szablonie.

Dokończenie zadania

Reszta rozwiązania będzie bardzo podobna do powyższych kroków. Nie bój się eksperymentować i używać console.log do sprawdzania, czy jesteś na dobrym tropie.

Dzięki tym zmianom cały kod HTML znajdzie się pliku HTML, a nasz kod JS będzie korzystał z szablonów. To bardzo przydatna umiejętność, którą będziemy wykorzystywać w kolejnych modułach.

7.5. Podsumowanie

W tym module poznaliśmy tablice i obiekty oraz nauczyliśmy się z nich korzystać. Dzięki nim mogliśmy wygenerować listę tagów i autorów oraz wyświetlić liczby artykułów przypisanych do nich. W przypadku tagów wykorzystaliśmy te informacje do stworzenia chmury, która wizualnie pokazuje, które tagi są najbardziej popularne.

Ten moduł, wraz z poprzednim, mogły być dla Ciebie ciężkie. Nie zdziwilibyśmy się, gdyby teraz pękała Ci głowa od tych wszystkich zmiennych, pętli i szablonów. A zaczęło się tak fajnie od układania kodu z klocków...

Nie przejmuj się! Pierwsze zetknięcie z JS-em może przyprawiać o zawrót głowy! W trzy tygodnie udało nam się przerobić olbrzymi zakres materiału! Zamiast uczyć się o różnych typach danych i wkuwać przydatne nazwy funkcji, skupiliśmy się na podejściu praktycznym.

Dzięki temu udało nam się napisać całkiem niezłą aplikację – bo nasz blog nie jest zwykłą stroną, ale tzw. single page application (SPA), która działa płynnie, bez przeładowania strony. Co więcej, nasze zastosowanie skryptów pozwoliło na uniknięcie redundancji, czyli niepotrzebnych powtórzeń. Każda informacja – np. tytuł artykułu czy jego autor – znajduje się w kodzie tylko raz. Wszystkie kolejne wystąpienia, np. w lewej i prawej kolumnie, są generowane na podstawie danych źródłowych.

Może zastanawiać Cię, czy dobrą praktyką jest umieszczanie wszystkich postów w kodzie HTML. Oczywiście, przy większej ilości postów, nie będzie to dobre rozwiązanie. Dlatego w następnym module poznamy zagadnienia AJAX i API. Dzięki nim dane – takie jak posty – będą pobierane z serwera tylko wtedy, kiedy będą potrzebne.

Pomyśl teraz przez chwilę jak działają serwisy takie jak GMail, Facebook, Twitter, czy Instagram. Są one o wiele bardziej skomplikowane, ale w swojej naturze są podobne do naszego bloga – również działają bez przeładowania strony. Następny moduł pozwoli nam również komunikować się z serwerem, co otworzy przed nami kolejne funkcjonalności znane z tych najpopularniejszych serwisów.

7.6. Quiz powtórkowy

Na koniec tego modułu przygotowaliśmy dla Ciebie quiz powtórkowy. Pomoże Ci on powtórzyć wiedzę z poprzednich modułów.

Odpowiedzi tego quizu nie są nigdzie zapisywane, więc są tylko do Twojej wiadomości. Ten quiz ma Ci posłużyć jako pomoc w nauce – dlatego pod każdym pytaniem znajdziesz guzik, który sprawdzi poprawność Twoich odpowiedzi oraz poda Ci wyjaśnienie zagadnienia poruszanego w tym pytaniu.

1. Zaznacz prawidłowe zdania dotyczące tablic:

Wyjaśnienie

Tablica:

  • jest zbiorem wartości ułożonych w jakiejś kolejności,
  • ma indeksy, czyli kolejne numery porządkowe swoich wartości,
  • posiada właściwość length,
  • może być stworzona za pomocą nawiasów kwadratowych [],
  • może zawierać dowolne typy wartości, w tym tablice i obiekty.

2. Zaznacz prawidłowe zdania dotyczące obiektów:

Wyjaśnienie

Obiekt:

  • jest zbiorem par klucz-wartości,
  • nie posiada indeksów,
  • nie posiada właściwości length,
  • może być stworzony za pomocą nawiasów klamrowych {},
  • może zawierać dowolne typy wartości, w tym tablice i obiekty.

3. Zaznacz prawidłowe zdania dotyczące pętli:

Wyjaśnienie

W przypadku pętli najważniejsze jest zrozumienie, czym jest i do czego można je wykorzystać. Szczegóły składni zawsze można znaleźć w internecie w momencie, gdy będziemy potrzebować pętli.

;